<script setup>
import AudioStreamPlayer from './utils/audioPlay.js';
import AudioStreamRecorder from './utils/audioStream.js';
import MarkdownRenderer from '@/components/markdown/index.vue';
import { buildCozeWebSocketUrl, safeCloseWebSocket } from '@/utils/cozeWebSocket';

const emit = defineEmits(['close', 'accept']);
const props = defineProps({
    agent: {
        type: Object,
        default: () => ({}),
    },
    token: {
        type: String,
        default: '',
    },
});

const { agent, token } = toRefs(props);

const mute = ref(false);
const showText = ref(true);
const isAccept = ref(false);
const speaker = ref(false);
const chatList = ref([]);
const chat_id = ref('');

let player = null;
let audioRecorder = null;
let ws = null;

onMounted(() => {
    player = new AudioStreamPlayer();
    audioRecorder = new AudioStreamRecorder();
    // #ifdef H5
    // 禁止滚动
    document.body.style.overflow = 'hidden';
    // #endif
});

onUnmounted(() => {
    player.destroy();
    audioRecorder.destroy();

    // 允许滚动
    if (ws) {
        safeCloseWebSocket(ws);
    }
    // #ifdef H5
    document.body.style.overflow = 'auto';
    // #endif
});

async function acceptFun() {
    isAccept.value = true;
    if (await checkRecordPermission()) {
        emit('accept');
    } else {
        closeFun();
    }
    nextTick(() => {
        // #ifdef MP-WEIXIN || APP-PLUS
        setTimeout(() => {
            const el = uni.createSelectorQuery().select('.chat_list');
            el.boundingClientRect(e => {
                console.log(e);
            }).exec();
        }, 1000);
        // #endif
    });
}

const checkRecordPermission = () => {
    return new Promise((resolve, reject) => {
        // 使用统一的录音工具类检查权限
        audioRecorder
            .checkRecordAuth()
            .then(result => {
                if (result) {
                    connectWebSocket();
                    resolve(true);
                } else {
                    uni.showToast({
                        title: '请允许使用麦克风权限以进行语音对话',
                        icon: 'none',
                        duration: 2000,
                    });
                    resolve(false);
                }
            })
            .catch(err => {
                uni.showToast({
                    title: '录音权限检查失败',
                    icon: 'none',
                    duration: 2000,
                });
                resolve(false);
            });
    });
};

const connectWebSocket = () => {
    const url = buildCozeWebSocketUrl(agent.value.bot_id, token.value);
    ws = uni.connectSocket({
        url,
        success: () => {
            console.log('WebSocket连接成功');
        },
    });
    ws.onOpen(() => {
        console.log('连接成功');
    });
    ws.onMessage(res => {
        const data = JSON.parse(res.data);
        console.log(data);

        if (data.event_type === 'chat.created') {
            const d = {
                id: new Date().getTime() + '',
                event_type: 'chat.update',
                data: {
                    input_audio: { format: 'pcm', codec: 'pcm', sample_rate: 24000 },
                    output_audio: { codec: 'pcm', pcm_config: { sample_rate: 24000 } },
                    turn_detection: {
                        type: 'server_vad',
                        silence_duration_ms: 1500,
                    },
                    need_play_prologue: true,
                },
            };
            startRecording();
            chatSend(d);
        } else if (data.event_type === 'conversation.audio_transcript.completed') {
            chatList.value.push({ role: 'user', text: data.data.content });
            player.stop();
        } else if (data.event_type === 'conversation.chat.canceled') {
            player.stop();
            if (chatList.value.length > 0 && !chatList.value[chatList.value.length - 1].text) {
                chatList.value[chatList.value.length - 1].text += '已取消';
            }
        } else if (data.event_type === 'conversation.chat.in_progress') {
            chatList.value.push({ role: 'assistant', text: '' });
        } else if (data.event_type === 'conversation.message.delta') {
            chatList.value[chatList.value.length - 1].text += data.data.content;
            nextTick(() => {
                // #ifdef H5
                const dom = document.querySelector('.call .call_center');
                if (dom) {
                    dom.scrollTo({ top: dom.scrollHeight, behavior: 'smooth' });
                }
                // #endif
            });
            if (!chat_id.value) {
                chat_id.value = data.data.conversation_id;
            }
        } else if (data.event_type === 'conversation.audio.delta') {
            if (data.data.content) {
                player.addAudio(data.data.content);
            }
        }
        if (
            data.event_type.includes('start') ||
            data.event_type.includes('delta') ||
            data.event_type.includes('audio.sentence_start')
        ) {
            speaker.value = true;
        } else {
            speaker.value = false;
        }
    });
    ws.onError(err => {
        console.log('WebSocket连接错误:', err);

        // 检查是否是签名或认证错误
        if (err.errMsg && err.errMsg.includes('Invalid HTTP status')) {
            console.log('检测到签名验证错误');
            uni.showToast({
                title: '认证失败，请重新连接',
                icon: 'none',
                duration: 3000,
            });
        } else {
            uni.showToast({
                title: '链接错误，请稍后重试',
                icon: 'none',
                duration: 3000,
            });
        }

        setTimeout(() => {
            closeFun();
        }, 3000);
    });
};

function chatSend(d) {
    ws.send({
        data: JSON.stringify(d),
    });
}

// 开始录音
const startRecording = async () => {
    try {
        await audioRecorder.startRecording(base64Data => {
            // 音频数据回调，发送到WebSocket
            const audioData = {
                id: new Date().getTime() + '',
                event_type: 'input_audio_buffer.append',
                data: { delta: base64Data },
            };
            chatSend(audioData);
        });
    } catch (error) {
        console.error('录音启动失败:', error);
        uni.showToast({
            title: error.message || '录音启动失败',
            icon: 'none',
            duration: 2000,
        });
        // 录音失败时关闭通话
        setTimeout(() => {
            closeFun();
        }, 2000);
    }
};

// 监听静音状态变化
watch(mute, newMute => {
    audioRecorder.setMute(newMute);
});

function closeFun() {
    if (chatList.value.length === 0) {
        return emit('close', null, []);
    }
    const data = [];
    mergeConsecutiveMessages(chatList.value.filter(item => item.text)).forEach(i => {
        if (i.role === 'user') {
            data.push({
                role: 'user',
                content: JSON.stringify([
                    {
                        type: 'text',
                        text: i.text,
                    },
                ]),
                type: 'question',
                content_type: 'object_string',
            });
        } else {
            data.push({
                role: 'assistant',
                content: i.text,
                follow_up: [],
                content_type: 'text',
            });
        }
    });

    const cozeChat = {
        title: chatList.value[0].text.slice(0, 30),
        cozeConversationId: chat_id.value,
        content: JSON.stringify(data),
        mode: 'coze',
        botId: agent.value.bot_id,
        createTime: new Date().getTime(),
        logo: agent.value.icon_url,
        name: agent.value.name,
    };

    emit('close', cozeChat, data);
}

function mergeConsecutiveMessages(messages) {
    if (!Array.isArray(messages) || messages.length === 0) {
        return [];
    }

    const result = [];
    let currentRole = messages[0].role;
    let currentText = messages[0].text;

    for (let i = 1; i < messages.length; i++) {
        const message = messages[i];

        if (message.role === currentRole) {
            // 合并相同角色的文本
            currentText += message.text;
        } else {
            // 推入前一个角色的合并消息
            result.push({
                role: currentRole,
                text: currentText,
            });

            // 开始新的角色消息
            currentRole = message.role;
            currentText = message.text;
        }
    }

    // 推入最后一个角色的消息
    result.push({
        role: currentRole,
        text: currentText,
    });

    return result;
}

function cancel() {
    if (speaker.value) {
        const audioData = {
            id: new Date().getTime() + '',
            event_type: 'conversation.chat.cancel',
        };
        chatSend(audioData);
    }
}
</script>

<template>
    <div class="call">
        <u-navbar title="" placeholder bgColor="transparent" leftIcon=""></u-navbar>
        <div class="title">
            <div class="name">{{ agent?.name }}</div>
            <div class="subtitles" @click="showText = !showText">
                字幕{{ showText ? '开' : '关' }}
            </div>
        </div>
        <div class="avatar" v-if="chatList.length === 0">
            <image :src="agent?.icon_url" mode="aspectFill"></image>
        </div>
        <div class="call_center" v-if="showText" id="call_center">
            <template v-for="(item, index) in chatList" :key="index">
                <div
                    class="item"
                    :class="item.role"
                    v-if="item.role === 'assistant' && index === chatList.length - 1"
                >
                    <div class="text">
                        <MarkdownRenderer
                            v-if="item.text"
                            :source="item.text"
                            style="width: 100%"
                        />
                        <span v-else-if="index === chatList.length - 1">正在思考中</span>
                    </div>
                </div>
                <div class="item" :class="item.role" v-else-if="item.text">
                    <div class="text">
                        <MarkdownRenderer
                            v-if="item.text"
                            :source="item.text"
                            style="width: 100%"
                        />
                        <span v-else-if="index === chatList.length - 1">正在思考中</span>
                    </div>
                </div>
            </template>
        </div>
        <div class="bottom" :style="{ gap: isAccept ? '30px' : '80px' }">
            <template v-if="!isAccept">
                <image src="/static/chat/close.png" @click="closeFun"></image>
                <div class="accept" @click="acceptFun">
                    <u-icon name="phone-fill" size="30px" color="#fff"></u-icon>
                </div>
            </template>
            <template v-else>
                <div class="icon" @click="mute = !mute">
                    <image :src="mute ? '/static/chat/mic-0.png' : '/static/chat/mic.png'"></image>
                </div>
                <div class="center1" @click="cancel">
                    <template v-if="mute">
                        <span>你已静音</span>
                    </template>
                    <template v-else>
                        <template v-if="!speaker">
                            <image src="/static/chat/record.svg" class="speaker"></image>
                            <span>正在听...</span>
                        </template>
                        <template v-else>
                            <wd-icon name="stop-circle" size="35px"></wd-icon>
                            <span>说话或点击打断</span>
                        </template>
                    </template>
                </div>
                <div class="icon" @click="closeFun">
                    <image src="/static/chat/stop.png"></image>
                </div>
            </template>
        </div>
    </div>
</template>

<style scoped lang="scss">
.call {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100vh;
    background: linear-gradient(#fed8e3 0%, #d1f1fe 100%);
    z-index: 1000000000;
    backdrop-filter: blur(10px);
    overflow: hidden;

    .call_center {
        width: 80%;
        max-height: 400px;
        overflow-y: auto;
        display: flex;
        flex-direction: column;
        gap: 10px;
        margin: 0 auto;
        margin-top: 40px;

        &::-webkit-scrollbar {
            display: none;
        }

        .item {
            display: flex;

            .text {
                padding: 5px 10px;
                border-radius: 8px;
                text-align: justify;

                :deep(p) {
                    margin: 0 !important;
                }
            }
        }

        .user {
            justify-content: flex-end;
            color: #73787c;

            .text {
                border-color: #6e16fa;
            }
        }
    }

    /* 背景模糊 */
    .title {
        display: flex;
        align-items: center;
        justify-content: center;
        position: relative;

        .name {
            max-width: 180px;
            text-align: center;
        }

        .subtitles {
            position: absolute;
            right: 20px;
            background: #fff;
            padding: 4px 10px;
            border-radius: 8px;

            &:active {
                background: #f5f5f5;
            }
        }
    }

    .avatar {
        display: flex;
        align-items: center;
        justify-content: center;
        margin-top: 20px;

        image {
            width: 100px;
            height: 100px;
            border-radius: 50%;
        }
    }

    .bottom {
        position: absolute;
        left: 0;
        bottom: 100px;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 100%;

        image,
        .accept {
            width: 70px;
            height: 70px;
        }

        .accept {
            display: flex;
            align-items: center;
            justify-content: center;
            background: green;
            border-radius: 50%;
        }

        .icon {
            display: flex;
            align-items: center;
            justify-content: center;
            background: #fff;
            width: 70px;
            height: 70px;
            border-radius: 50%;

            &:active {
                background: #f5f5f5;
            }

            image {
                width: 30px;
                height: 30px;
            }
        }

        .center1 {
            width: 100px;
            text-align: center;
            color: #73787c;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 5px;
            flex-direction: column;
            font-size: 14px;

            image {
                width: 40px;
                height: 30px;
            }
        }
    }
}
</style>
